package com.vivo.ic.dm;

import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.ContentObserver;
import android.database.Cursor;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
import android.os.Process;
import android.text.format.DateUtils;

import com.vivo.ic.VLog;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class DownloadService extends Service {
    private static final String TAG = Constants.PRE_TAG + "DownloadService";
    
    private static final int MSG_UPDATE = 1;
    private static final int MSG_FINAL_UPDATE = 2;
    
    // service每次被启动，此值都不相同。
    private volatile int mLastStartId;
    
    private AlarmManager mAlarmManager;
    private DownloadScanner mScanner;
    private DownloadManagerContentObserver mObserver;

    /**
     * DownloadService使用线程来有序处理handler的message信息，保证顺序的同时，也防止了updateThread被过度频繁调用
     */
    private HandlerThread mUpdateThread;
    private Handler mUpdateHandler;
    
    private final ExecutorService mExecutor = buildDownloadExecutor();

    @SuppressLint("UseSparseArrays")
    private final HashMap<Long, DownloadInfo> mDownloads = new HashMap<Long, DownloadInfo>();
    
    private static ExecutorService buildDownloadExecutor() {
        
        // 最大可同时下载数
        final int maxConcurrent = DownloadManager.getConfig().getMaxConcurrentDownloadsAllowed();

        // Create a bounded thread pool for executing downloads; it creates
        // threads as needed (up to maximum) and reclaims them when finished.
        final ThreadPoolExecutor executor = new ThreadPoolExecutor(
                maxConcurrent, maxConcurrent, 10, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>()) {
            @Override
            protected void afterExecute(Runnable r, Throwable t) {
                super.afterExecute(r, t);

                if (t == null && r instanceof Future<?>) {
                    try {
                        ((Future<?>) r).get();
                    } catch (CancellationException ce) {
                        t = ce;
                    } catch (ExecutionException ee) {
                        t = ee.getCause();
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                    }
                }

                if (t != null) {
                    VLog.w(TAG, "Uncaught exception", t);
                }
            }
        };
        executor.allowCoreThreadTimeOut(true);
        return executor;
    }
    
    private Handler.Callback mUpdateCallback = new Handler.Callback() {
        
        @Override
        public boolean handleMessage(Message msg) {
            
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

            final int startId = msg.arg1;

            final boolean isActive;
            synchronized (mDownloads) {
                isActive = updateLocked();
            }

            if (msg.what == MSG_FINAL_UPDATE) {
                for (Map.Entry<Thread, StackTraceElement[]> entry :
                        Thread.getAllStackTraces().entrySet()) {
                    if (entry.getKey().getName().startsWith("pool")) {
                        VLog.w(TAG, "Final update pass !!! " + entry.getKey() + ": " + Arrays.toString(entry.getValue()));
                    }
                }

                VLog.w(TAG, "Final update pass triggered, isActive=" + isActive
                        + "; someone didn't update correctly.");
            }

            if (isActive) {
                enqueueFinalUpdate();

            } else {

                if (stopSelfResult(startId)) {
                    VLog.v(TAG, "Nothing left; stopped");
                    getContentResolver().unregisterContentObserver(mObserver);
                    mScanner.shutdown();
                    mUpdateThread.quit();
                }
            }

            return true;
        }
    };
    
    /**
     * 数据库数据每次发生改变就会发送一个MSG_UPDATE给mUpdateHandler。
     */
    private class DownloadManagerContentObserver extends ContentObserver {
        public DownloadManagerContentObserver() {
            super(new Handler());
        }

        @Override
        public void onChange(final boolean selfChange) {
            VLog.d(TAG,"ContentObserver onChange");
            enqueueUpdate();
        }
    }
    
    @Override
    public void onCreate() {
        super.onCreate();
        VLog.d(TAG, "DownloadService onCreate");
        
        mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
        mScanner = new DownloadScanner(this);
        
        mObserver = new DownloadManagerContentObserver();
        getContentResolver().registerContentObserver(Downloads.Impl.CONTENT_URI, true, mObserver);
        
        mUpdateThread = new HandlerThread(TAG + "-UpdateThread");
        mUpdateThread.start();
        mUpdateHandler = new Handler(mUpdateThread.getLooper(), mUpdateCallback);
    }
    
    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Cannot bind to Download Manager Service");
    }
    
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        int returnValue = super.onStartCommand(intent, flags, startId);
        VLog.v(TAG, "Service onStartCommand with mLastStartId: " + startId);
        mLastStartId = startId;
        enqueueUpdate();
        return returnValue;
    }
    
    @Override
    public void onDestroy() {
        getContentResolver().unregisterContentObserver(mObserver);
        mScanner.shutdown();
        mUpdateThread.quit();
        VLog.v(TAG, "Service onDestroy");
        super.onDestroy();
    }
    
    public void enqueueUpdate() {
        if (mUpdateHandler != null) {
            mUpdateHandler.removeMessages(MSG_UPDATE);
            mUpdateHandler.obtainMessage(MSG_UPDATE, mLastStartId, -1).sendToTarget();
        }
    }
    
    /**
     * 正常情况，service中的message是随下载被循环发送的，保证数据实时更新，当mUpdateCallback认为已经无需更新时，就会关闭
     * service并退出handler的事物处理thread。一旦MSG_FINAL_UPDATE在5分钟之后被送到了callback中，那就说明出现了问题，
     * 需要关注此处的log。(比如isActive判定出错)
     */
    private void enqueueFinalUpdate() {
        mUpdateHandler.removeMessages(MSG_FINAL_UPDATE);
        mUpdateHandler.sendMessageDelayed(
                mUpdateHandler.obtainMessage(MSG_FINAL_UPDATE, mLastStartId, -1),
                5 * DateUtils.MINUTE_IN_MILLIS);
    }
    
    private boolean updateLocked() {
        final long now = System.currentTimeMillis();
        
        boolean isActive = false;
        long nextActionMillis = Long.MAX_VALUE;
        
        // 遍历数据库，遍历一个删一个，最后还剩下的就是孤立的冗余数据，需要删除
        ArrayList<Long> staleIds = new ArrayList<Long>();
        for (Long id : mDownloads.keySet()) {
            staleIds.add(id);
        }
        
        final ContentResolver resolver = getContentResolver();
        final Cursor cursor = resolver.query(Downloads.Impl.CONTENT_URI,
                null, null, null, null);
        try {
            final DownloadInfo.Reader reader = new DownloadInfo.Reader(resolver, cursor);
            final int idColumn = cursor.getColumnIndexOrThrow(Downloads.Column._ID);
            
            while (cursor.moveToNext()) {
                final long id = cursor.getLong(idColumn);
                staleIds.remove(id);
                
                DownloadInfo info = mDownloads.get(id);

                if (info != null) {
                    VLog.v(TAG, "DownloadInfo when get: " + info);
                    updateDownload(reader, info, now);
                    VLog.v(TAG, "DownloadInfo after update: " + info);
                } else {
                    info = insertDownloadLocked(reader, now);
                }
                
                final boolean activeDownload = info.startDownloadIfReady(mExecutor);

                info.startScanIfReady(mScanner);

                isActive |= activeDownload;
                
                nextActionMillis = Math.min(info.nextActionMillis(now), nextActionMillis);
                
                if (activeDownload) {
                    VLog.v(TAG, "activeDownload is " + info);
                } else {
                    VLog.v(TAG, "diedDownload is " + info);
                }
                
                // 如果有必要，记录上次的网络状态
//                int netValueLast = DownloadManager.getInstance().getLastNetType();
//                int netValueNow;
//                if (netValueLast > 0) {
//                    netValueNow = netValueLast;
//                } else {
//                    netValueNow = DMUtil.getNetValueNow();
//                }
//
//                if (needUpdateNetChangedInfo(getContentResolver(), info, netValueNow)) {
//                    ContentValues values = new ContentValues();
//                    values.put(Downloads.Column.NETWORK_CHANGED, netValueNow);
//                    resolver.update(info.getDownloadsUri(), values, null, null);
//                }
            }
            
        } finally {
            cursor.close();
        }
        
        // 删除孤立的冗余数据
        for (Long id : staleIds) {
            deleteDownloadLocked(id);
        }
        
        DownloadManager.getInstance().getNotifier().updateWith(mDownloads.values());

        if (nextActionMillis > 0 && nextActionMillis < Long.MAX_VALUE) {
            VLog.v(TAG, "scheduling start in " + nextActionMillis + "ms");

            final Intent intent = new Intent(Downloads.Action.DOWNLOAD_RETRY);
            intent.setClass(this, DownloadReceiver.class);
            // 使用硬件闹钟来唤醒下载管理器进行重试操作
            mAlarmManager.set(AlarmManager.RTC_WAKEUP, now + nextActionMillis,
                    PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_ONE_SHOT));
        }

        return isActive;
    }
    
    private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info, long now) {
        reader.updateFromDatabase(info);
        VLog.v(TAG, "reader.updateFromDatabase(info) " + info.mId + " now: " + now);
    }
    
    private DownloadInfo insertDownloadLocked(DownloadInfo.Reader reader, long now) {
        final DownloadInfo info = reader.newDownloadInfo(this);
        mDownloads.put(info.mId, info);

        VLog.v(TAG, "processing inserted download " + info.mId + " now: " + now);

        return info;
    }
    
    private void deleteDownloadLocked(long id) {
        VLog.v(TAG, "deleteDownloadLocked of id:" + id);
        DownloadInfo info = mDownloads.get(id);
        if (info.mStatus == Downloads.Impl.STATUS_RUNNING) {
            info.mStatus = Downloads.Impl.STATUS_CANCELED;
        }
        mDownloads.remove(info.mId);
    }
    
    /**
     * 下载管理器在遍历cursor的过程中如果DB数据发生改变，会导致cursor数据与实际数据不匹配，这会导致一些低概率的问题。
     * 故此处直接实时查询数据库数据，修正此类问题。
     * @param info
     * @param netValueNow
     * @return
     */
    private boolean needUpdateNetChangedInfo(ContentResolver cr, DownloadInfo info, int netValueNow) {
        Cursor cursor = null;
        try {
            cursor = cr.query(info.getDownloadsUri(),
                    new String[] { Downloads.Column.NETWORK_CHANGED, Downloads.Column.CONTROL },
                    null, null, null);
            if (cursor != null && cursor.moveToFirst()) {
                int changedReal = cursor.getInt(0);
                int control = cursor.getInt(1);
                if ((changedReal == 1 && control == Downloads.Impl.CONTROL_RUN)
                        || (changedReal == 0 && control == Downloads.Impl.CONTROL_RUN)
                        || ((netValueNow != 0 && netValueNow != -1) && changedReal != 1 && changedReal != netValueNow)) {
                    VLog.w(TAG, "id:" + info.mId + " status changed from " + changedReal + " to " + netValueNow);
                    return true;
                }
            } else {
                VLog.w(TAG, "getChangedFromDb no record of id " + info.mId);
            }
        } catch (Exception exception) {
            VLog.w(TAG, "error in getChangedFromDb of id " + info.mId, exception);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return false;
    }
}
